在不違反封裝性的前提下,捕捉物件的內部狀態並存在外面,以便日後回復至此一狀態。
希望讀者也有過類似經驗,以免顯得我老了(QQ)。話說以前玩單機 RPG 的時候,像是金庸群俠傳,仙劍奇俠傳之類的遊戲,最麻煩的就是,好不容易解完了謎題,要打魔王了,才發現,等級不夠,或者是某個技能沒練滿打不過!小 case,讓我運一下苦練已久的 S(ave) / L(oad) 大法吧!你知道的,等級不夠就是回頭去衝等再來過吧。
想像你製作了一個簡報軟體,除了可以打上文字以外,還要可以讓使用者加入和編排幾何圖形。例如他可以加入像下面這張圖的樣子,兩個方塊有一條連接線。
有一天,使用者調整了一下這兩個圖示的編排,變成了下面這樣。
但是,他後悔了,他想要回到原本的編排,而剛剛好我們的程式提供了上一步的功能,能夠完美的恢復投影片變成
而不是
持續地另存新檔,然後開啟舊檔
以簡報軟體來說,每一張投影片都會有該張投影片所擁有的內容,可能是文字,也可能是圖形,也會有圖形和圖形之間的連結,如果我們的程式可以把當前投影片的狀態全部記錄下來,當我們需要回復的時候,直接還原到先前紀錄的狀態就可以了。
而設計模式中的 Memento 模式,就是在為這種類型的情境所提供的套路,下面就讓我們來看看 Memento 的設計吧。
廢話不多說,先上圖!
啥?什麼 Originator?什麼 Caretaker?這也太多新名詞了吧。Originator 就是產生 Memento 的物件,而 Caretaker 是知道什麼時候要取得 Memento 以及還原的物件。這樣說吧,以前面的簡報軟體來說,編輯器就是 Caretaker,而每一張投影片乃至於每一個圖形都可以是 Originator。
回到 Memento 的定義,有一個很重要的要點是「不違反封裝性」,我們可以思考,當我們的簡報軟體要達成上一步的時候,會是由哪個類別去完成?如果是由編輯器(Caretaker)來負責的話,貌似放了太多責任在編輯器(Caretaker)上,而且也會暴露過多投影片(Originator)的內部狀態,並且也提供了過多可以更動投影片(Originator)的介面。如果這樣設計的話,會導致程式的相依性過高,未來維護的時候就難以改動了。因此 Memento 模式在設計上,並不讓 Caretaker 知道該如何復原狀態,Caretaker 只要知道何時要恢復並呼叫誰(Originator)去恢復。
另外一點是,為了保護 Memento 的狀態不被更動,Memento 其實會是個 Value Object,所有的資料都在被建構的時候傳入,除此之外,不會有任何介面可以去更動內部的資料。想想看,如果使用 S/L 大法的時候,讀取回來的遊戲進度完全不同了,會不會很驚喜(?)。(嘛,喜歡嘗試破解遊戲存擋的例外)
下面就來看看三種用 Java 實作的方式吧
public class Caretaker {
private Originator originator;
private Stack<Memento> history;
public Caretaker(Origniator originator) {
this.originator = originator;
history = new Stack<Memento>();
}
private void doSomething() {
Memento m = originator.save();
history.push(m);
}
private void undo() {
Memento m = history.pop();
originator.restore(m);
}
}
// Imagine this is the class holds the data and states of slides
public class State {}
public class Originator {
private State state;
public Memento save() {
new Memento(state);
}
public void restore(Memento m) {
State state = m.getState();
this.state = state;
// notify state changed
}
public class Memento {
private State state;
private Memento(State state) {
this.state = state;
}
private State getState() {
return state;
}
}
}
通常為不支援巢狀類別的語言所採用,例如 PHP。
public class Caretaker {
private Originator originator;
private Stack<Memento> history;
public Caretaker(Origniator originator) {
this.originator = originator;
history = new Stack<Memento>();
}
private void doSomething() {
Memento m = originator.save();
history.push(m);
}
private void undo() {
Memento m = history.pop();
originator.restore(m);
}
}
// Imagine this is the class holds the data and states of slides
public class State {}
public interface Memento {
}
public class Originator {
private State state;
public Memento save() {
new ConcreteMemento(state);
}
public void restore(Memento m) {
ConcreteMemento cm = (ConcreteMemento) m;
State state = m.getState();
this.state = state;
// notify state changed
}
}
public class ConcreteMemento implements Memento {
private State state;
public ConcreteMemento(State state) {
this.state = state;
}
public State getState() {
return state;
}
}
public class Caretaker {
private Stack<Memento> history;
public Caretaker() {
history = new Stack<Memento>();
}
private void undo() {
Memento m = history.pop();
m.restore();
}
}
// Imagine this is the class holds the data and states of slides
public class State {}
public interface Memento {
public void restore();
}
public interface Originator {
public Memento save();
}
public class ConcreteOriginator implements Originator {
private State state;
public Memento save() {
new ConcreteMemento(state);
}
public void setState(State state) {
this.state = state;
// notify state changed
}
}
public class ConcreteMemento implements Memento {
private State state;
private Originator originator;
private ConcreteMemento(Originator originator, State state) {
this.originator = originator;
this.state = state;
}
public void restore() {
originator.setState(state);
}
}
透過這個設計,我們能夠讓 Originator 和 Memento 有更多不同的實作,卻又能夠讓外部程式使用,並且可以維持好的封裝。
undo
功能時,可以使用 Memento 模式。這樣的話,Command 模式的命令就相當於 Memento 中的 Caretaker。Memento
Dive into Design Pattern - Memento
作者:Yenting